﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Fadd.System
{
    /// <summary>
    /// Searches through assemblies after the specified types.
    /// </summary>
    /// <remarks>
    /// Loads an seperate appdomain to perform the scanning. No assemblies are
    /// loaded into the application AppDomain thanks to this.
    /// </remarks>
    public class AssemblyTypeScanner
    {
        private List<TypeInformation> _items = new List<TypeInformation>();

        /// <summary>
        /// Initializes a new instance of the <see cref="AssemblyTypeScanner"/> class.
        /// </summary>
        public AssemblyTypeScanner()
        {
            _items = new List<TypeInformation>();
        }

        /// <summary>
        /// Gets all found types
        /// </summary>
        public IEnumerable<TypeInformation> Items
        {
            get { return _items; }
        }

        /// <summary>
        /// Adds the specified plugin.
        /// </summary>
        /// <param name="plugin">The plugin.</param>
        /// <param name="assembly">Assembly that the plugin resides in.</param>
        /// <param name="type">Plugin interface type.</param>
        /// <param name="publicKey">Public key, should be used to decide the amount of access for the module.</param>
        protected void Add(ref TypeInformation plugin, Assembly assembly, Type type, byte[] publicKey)
        {
            if (plugin == null)
            {
                plugin = new TypeInformation(assembly.Location, type, publicKey);
                _items.Add(plugin);
            }
            else
                plugin.Add(type);
        }


        /// <summary>
        /// Finds the specified full path.
        /// </summary>
        /// <param name="fullPath">Path and wildcards.</param>
        /// <param name="types">Types wanted.</param>
        /// <example>
        /// <code>
        /// List&gt;Type&lt; types = new List&gt;Type&lt;();
        /// types.Add(typeof(IPlayer));
        /// types.Add(typeof(IViewer));
        /// finder.Find("C:\\myapp\\plugins\\plugin*.dll", types);
        /// </code>
        /// </example>
        /// <exception cref="ReflectionTypeLoadException"></exception>
        /// <exception cref="TypeLoadException"></exception>
        public void Find(string fullPath, params Type[] types)
        {
            AppDomain domain = AppDomain.CreateDomain("ModuleLoader");

            var finder = (AssemblyTypeScanner) domain.CreateInstanceFromAndUnwrap(
                GetType().Assembly.Location,
                GetType().FullName);

            finder.FindInternal(fullPath, types);
            _items = finder._items;

            AppDomain.Unload(domain);
        }

        /// <summary>
        /// Finds the specified full path.
        /// </summary>
        /// <param name="fullPath">Path and wildcards.</param>
        /// <param name="types">Types wanted.</param>
        /// <example>
        /// <code>
        /// List&gt;Type&lt; types = new List&gt;Type&lt;();
        /// types.Add(typeof(IPlayer));
        /// types.Add(typeof(IViewer));
        /// finder.Find("C:\\myapp\\plugins\\plugin*.dll", types);
        /// </code>
        /// </example>
        /// <exception cref="TypeLoadException"></exception>
        protected void FindInternal(string fullPath, IEnumerable<Type> types)
        {
            string searchString;
            string directory;
            if (fullPath.Contains("."))
            {
                int pos = fullPath.LastIndexOf('\\');
                if (pos == -1)
                {
                    searchString = fullPath;
                    directory = Environment.CurrentDirectory;
                }
                else
                {
                    searchString = fullPath.Substring(pos + 1);
                    directory = fullPath.Substring(0, pos);
                }
            }
            else
            {
                searchString = "*.dll";
                directory = fullPath;
            }

            foreach (string fileName in Directory.GetFiles(directory, searchString))
            {
                try
                {
                    Assembly asm;
                    byte[] publicKey;
                    try
                    {
                        asm = Assembly.LoadFrom(fileName);
                        AssemblyName assemblyName = asm.GetName();
                        publicKey = assemblyName.GetPublicKey();
                    }
                    catch (FileLoadException err)
                    {
                        global::System.Console.WriteLine(err);
                        continue;
                    }


                    ScanAssembly(asm, types, publicKey);
                }
                catch (ReflectionTypeLoadException e)
                {
                    throw new TypeLoadException("Couldn't load plugin: " + fileName, e);
                }
            }
        }

        private void ScanAssembly(Assembly asm, IEnumerable<Type> types, byte[] publicKey)
        {
            TypeInformation plugin = null;
            foreach (Type wantedType in types)
            {
                Type[] assemblyTypes;
                try
                {
                    assemblyTypes = asm.GetTypes();
                }
                catch (ReflectionTypeLoadException err)
                {
                    global::System.Console.WriteLine(err);
                    continue;
                }


                foreach (Type type in assemblyTypes)
                {
                    if (wantedType.IsAssignableFrom(type) && !type.IsInterface)
                        Add(ref plugin, asm, type, publicKey);
                }
            }
        }
    }
}